"""Like responses, but remote."""
import argparse
import collections
import contextlib
from http import server
import os
import pickle
import typing
from unittest import mock
from urllib import parse
import uuid

import requests
import responses
import sentry_sdk

from . import cluster
from .. import logger
from .. import metrics
from .. import misc
from .. import session

LOGGER = logger.get_logger('cki_lib.inttests.remote_responses')
SESSION = session.get_session('cki_lib.inttests.remote_responses')


class _CallProxy:
    # pylint: disable=too-few-public-methods
    """Proxy to avoid anonymous local methods."""

    target: collections.abc.Callable[..., typing.Any]
    args: tuple[typing.Any]
    kwargs: dict[str, typing.Any]

    def __init__(
        self,
        target: collections.abc.Callable[..., typing.Any],
        *args: typing.Any,
        **kwargs: typing.Any,
    ):
        """Initialize the proxy."""
        self.target = target
        self.args = args
        self.kwargs = kwargs

    def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
        return self.target(*self.args, **self.kwargs)(*args, **kwargs)


class _BaseResponseProxy:
    # pylint: disable=too-few-public-methods
    """Wrapper for BaseResponse."""

    def __init__(self, url: str, *args: typing.Any) -> None:
        """Wrap a BaseResponse."""
        self.url = url
        self.args = args

    def __getattr__(self, name: str) -> typing.Any:
        response = SESSION.post(f'{self.url}/.mock/get',
                                data=pickle.dumps({'args': self.args}))
        return getattr(pickle.loads(response.content), name)


class RemoteResponsesHelper:
    """Responses-compatible API helper."""

    def __init__(self, url: str) -> None:
        """Responses-compatible API helper."""
        self.url = url

    def _proxy(
        self,
        endpoint: str,
        result_expected: bool,
        *args: typing.Any,
        **kwargs: typing.Any,
    ) -> typing.Any:
        response = SESSION.post(f'{self.url}/.mock/{endpoint}',
                                data=pickle.dumps({'args': args, 'kwargs': kwargs}))
        return pickle.loads(response.content) if result_expected else None

    @property
    def calls(self) -> typing.Any:
        """Proxy to responses.calls."""
        return self._proxy('calls', True)

    def add(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
        """Proxy to responses.add."""
        return _BaseResponseProxy(self.url, self._proxy('add', True, *args, **kwargs))

    def replace(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
        """Proxy to responses.replace."""
        return _BaseResponseProxy(self.url, self._proxy('replace', True, *args, **kwargs))

    def upsert(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
        """Proxy to responses.upsert."""
        return _BaseResponseProxy(self.url, self._proxy('upsert', True, *args, **kwargs))

    def reset(self) -> typing.Any:
        """Proxy to responses.reset."""
        return self._proxy('reset', False)

    @staticmethod
    def json_params_matcher(
        params: dict[str, typing.Any] | list[typing.Any] | None,
        *,
        strict_match: bool = True,
    ) -> collections.abc.Callable[[requests.PreparedRequest], tuple[bool, str]]:
        """Proxy for responses.matchers.json_params_matcher."""
        return _CallProxy(responses.matchers.json_params_matcher, params, strict_match=strict_match)


class RemoteResponsesServer(cluster.KubernetesCluster):
    """Add a RemoteResponses server to the mix."""

    responses: RemoteResponsesHelper

    @classmethod
    def setUpClass(cls) -> None:
        """Set up the service."""
        super().setUpClass()
        cls.enterClassContext(cls._remote_responses())

    def setUp(self) -> None:
        """Clear the mocked responses."""
        super().setUp()
        self.responses.reset()

    @classmethod
    @contextlib.contextmanager
    def _remote_responses(cls) -> collections.abc.Iterator[None]:
        service_id = 'remote-responses'
        now = misc.now_tz_utc()
        with cls.k8s_namespace(service_id):
            LOGGER.info('Starting Remote Responses server')
            cls.k8s_deployment(namespace=service_id, name=service_id, setup_at=now, container={
                'image': 'quay.io/cki/remote-responses',
                'startupProbe': cls.k8s_startup_probe(7999),
                'env': [
                    {'name': 'CKI_LOGGING_LEVEL', 'value': 'DEBUG'},
                ],
            })
            cls.k8s_service(namespace=service_id, name=service_id)
            if not cls.k8s_wait(namespace=service_id, name=service_id, setup_at=now):
                raise Exception(f'{service_id} did not start up')

            cls.responses = RemoteResponsesHelper(f'http://{cls.hostname}:7999')

            with mock.patch.dict(os.environ, {
                'REMOTE_RESPONSES_URL': f'http://{service_id}.{service_id}.svc.cluster.local:7999',
                'REMOTE_RESPONSES_URL_EXTERNAL': f'http://{cls.hostname}:7999',
            }):
                yield


REQUESTS_MOCK = responses.RequestsMock()
RESPONSE_DICT: dict[str, responses.BaseResponse] = {}


class _MockRequestHandler(server.BaseHTTPRequestHandler):
    """Provide remote mocking."""

    @staticmethod
    def _proxy_call(
        what: collections.abc.Callable[..., responses.BaseResponse],
        method: str,
        url: str,
        *args: typing.Any,
        **kwargs: typing.Any,
    ) -> str:
        url = parse.urlsplit(url)._replace(netloc='hostname', scheme='http').geturl()
        LOGGER.debug('Calling %s %s %s %s %s', what, method, url, args, kwargs)
        response = what(method, url, *args, **kwargs)
        response_id = str(uuid.uuid4())
        RESPONSE_DICT[response_id] = response
        return response_id

    def _proxy(self, what: collections.abc.Callable[..., responses.BaseResponse]) -> str:
        data = pickle.load(self.rfile)
        return self._proxy_call(what, *data['args'], **data['kwargs'])

    def handle_meta(self) -> None:
        """Respond to a single meta HTTP request."""
        LOGGER.debug('Handling meta request at %s', self.path)
        result: typing.Any = None
        match self.path:
            case '/.mock/get':
                result = RESPONSE_DICT[pickle.load(self.rfile)['args'][0]]
            case '/.mock/calls':
                result = REQUESTS_MOCK.calls
            case '/.mock/reset':
                REQUESTS_MOCK.reset()
                RESPONSE_DICT.clear()
            case '/.mock/add':
                result = self._proxy(REQUESTS_MOCK.add)
            case '/.mock/replace':
                result = self._proxy(REQUESTS_MOCK.replace)
            case '/.mock/upsert':
                result = self._proxy(REQUESTS_MOCK.upsert)
        self.send_response(200)
        self.send_header('Content-type', 'application/octet-stream')
        self.end_headers()
        pickle.dump(result, self.wfile)

    def handle_mocked(self) -> None:
        """Respond to a single HTTP request by relaying to responses."""
        LOGGER.debug('Forwarding %s %s to responses', self.command, self.path)
        data = self.rfile.read(int(self.headers.get('Content-Length', '0')))
        response = SESSION.request(
            self.command, f'http://hostname{self.path}', data=data, headers={**self.headers})
        self.send_response(response.status_code)
        for keyword, value in response.headers.items():
            self.send_header(keyword, value)
        self.end_headers()
        self.wfile.write(response.text.encode('utf8'))

    def handle_one_request(self) -> None:
        # pylint: disable=attribute-defined-outside-init
        """Handle a single HTTP request."""
        try:
            self.raw_requestline = self.rfile.readline()
            if not self.raw_requestline:
                self.close_connection = True
                return
            if not self.parse_request():
                return
            if self.command == 'POST' and self.path.startswith('/.mock/'):
                self.handle_meta()
            else:
                self.handle_mocked()
            self.wfile.flush()
        except TimeoutError as err:
            LOGGER.error("Request timeout: %s", err)
            self.close_connection = True
            return


def main(args: list[str] | None = None) -> None:
    """CLI Interface."""
    parser = argparse.ArgumentParser()
    parser.parse_args(args)

    metrics.prometheus_init()
    server_address = ('', 7999)
    LOGGER.debug('Listening at %s', server_address)
    REQUESTS_MOCK.start()
    server.ThreadingHTTPServer(server_address, _MockRequestHandler).serve_forever()


if __name__ == '__main__':
    misc.sentry_init(sentry_sdk)
    main()
